<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> 
    <title> - 天地维杰网</title>
    <meta name="keywords" content="系统架构,shutdown,不与天斗,Domino,博客,程序员,架构师,笔记,技术,分享,java,Redis">
    
    <meta property="og:title" content="">
    <meta property="og:site_name" content="天地维杰网">
    <meta property="og:image" content="/img/author.jpg"> 
    <meta name="title" content=" - 天地维杰网" />
    <meta name="description" content="天地维杰网 | 博客 | 软件 | 架构 | Java "> 
    <link rel="shortcut icon" href="http://www.shutdown.cn/img/favicon.ico" />
    <link rel="apple-touch-icon" href="http://www.shutdown.cn/img/apple-touch-icon.png" />
    <link rel="apple-touch-icon-precomposed" href="http://www.shutdown.cn/img/apple-touch-icon.png" />
    <link href="http://www.shutdown.cn/js/vendor/font-awesome/css/font-awesome.min.css?v=4.6.2" rel="stylesheet" type="text/css" />
    <link href="http://www.shutdown.cn/js/vendor/fancybox/jquery.fancybox.css?v=2.1.5" rel="stylesheet" type="text/css" />
    <link href="http://www.shutdown.cn/css/main.css" rel="stylesheet" type="text/css" />
    <link href="http://www.shutdown.cn/css/syntax.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript" id="hexo.configuration">
  var NexT = window.NexT || {};
  var CONFIG = {
    scheme: 'Pisces',
    sidebar: {"position":"left","display":"post"},
     fancybox: true, 
    motion: true
  };
</script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7826003325059020" crossorigin="anonymous"></script>
</head>
<body itemscope itemtype="http://schema.org/WebPage" lang="zh-Hans">
<div class="container one-collumn sidebar-position-left page-home  ">
    <div class="headband"></div>

    <header id="header" class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner"> <div class="site-meta  custom-logo ">

  <div class="custom-logo-site-title">
    <a href="http://www.shutdown.cn"  class="brand" rel="start">
      <span class="logo-line-before"><i></i></span>
      <span class="site-title">天地维杰网</span>
      <span class="logo-line-after"><i></i></span>
    </a>
  </div>
  <p class="site-subtitle">人如秋鸿来有信，事若春梦了无痕</p>
</div>

<div class="site-nav-toggle">
  <button>
    <span class="btn-bar"></span>
    <span class="btn-bar"></span>
    <span class="btn-bar"></span>
  </button>
</div>

<nav class="site-nav">
    <ul id="menu" class="menu">
      
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-home"></i> <br />首页
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/redis/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-battery-full"></i> <br />Redis
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/java/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-coffee"></i> <br />java
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/linux/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-linux"></i> <br />linux
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/daily/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-bug"></i> <br />日常问题
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/spring/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-child"></i> <br />Spring和Springboot
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/mac/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-fire"></i> <br />Mac相关
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/middleware/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-gavel"></i> <br />中间件
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/jiagou/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-rocket"></i> <br />架构
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/python/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-ship"></i> <br />python
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/front/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-bolt"></i> <br />前端
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/jvm/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-balance-scale"></i> <br />jvm
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/c/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-battery-empty"></i> <br />c语言
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/categories/web3/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-web3"></i> <br />web3
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/post/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-archive"></i> <br />归档
          </a>
        </li>
      
        <li class="menu-item ">
          <a href="http://www.shutdown.cn/about/" rel="section">
              <i class="menu-item-icon fa fa-fw fa-user"></i> <br />关于
          </a>
        </li>
      
      <li class="menu-item menu-item-search">
        <a href="javascript:;" class="popup-trigger"> <i class="menu-item-icon fa fa-search fa-fw"></i> <br /> 搜索</a>
      </li>
    </ul>
    <div class="site-search">
      <div class="popup">
 <span class="search-icon fa fa-search"></span>
 <input type="text" id="local-search-input">
 <div id="local-search-result"></div>
 <span class="popup-btn-close">close</span>
</div>

    </div>
</nav>

 </div>
    </header>

    <main id="main" class="main">
      <div class="main-inner">
        <div class="content-wrap">
          <div id="content" class="content">
            
<section id="posts" class="posts-expand">
  <article class="post post-type-normal " itemscope itemtype="http://schema.org/Article">
    <header class="post-header">
      <h1 class="post-title" itemprop="name headline">
        <a class="post-title-link" href="http://www.shutdown.cn/post/redis%E5%AD%A6%E4%B9%A0%E6%96%87%E6%A1%A3/" itemprop="url">
        
        </a>
      </h1>
      <div class="post-meta">
      <span class="post-time">
<span class="post-meta-item-icon">
    <i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">时间：</span>
<time itemprop="dateCreated" datetime="2016-03-22T13:04:35+08:00" content="0001-01-01">
    0001-01-01
</time>
</span> 
      
      
       <span>
&nbsp; | &nbsp;
<span class="post-meta-item-icon">
    <i class="fa fa-eye"></i>
</span>
<span class="post-meta-item-text">阅读：</span>
<span class="leancloud-visitors-count">1610 字 ~8分钟</span>
</span>
      </div>
    </header>
    <div class="post-body" itemprop="articleBody">
    

    

<h1 id="redis学习文档">Redis学习文档</h1>

<h3 id="redis安装和启动">Redis安装和启动</h3>

<p>安装Redis</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">mkdir -p /usr/redis
<span style="color:#204a87">cd</span> /usr/redis
<span style="color:#8f5902;font-style:italic"># 下载redis源码包</span>
wget http://download.redis.io/releases/redis-3.2.9.tar.gz
tar -vxzf redis-3.2.9.tar.gz
ln -s redis-3.2.9 redis
<span style="color:#204a87">cd</span> redis 
<span style="color:#8f5902;font-style:italic"># 直接make会报错误， zmalloc.h:50:31: 致命错误：jemalloc/jemalloc.h：没有那个文件或目录</span>
<span style="color:#8f5902;font-style:italic"># 加 MALLOC=libc 参数可以正常编译</span>
make <span style="color:#000">MALLOC</span><span style="color:#ce5c00;font-weight:bold">=</span>libc
<span style="color:#8f5902;font-style:italic"># 将运行文件安装到 /usr/local/bin 下，可以在任意目录下执行redis命令</span>
make install</code></pre></div>
<p>启动Redis</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">redis-server 配置文件名称.conf</code></pre></div>
<p>客户端连接操作</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#8f5902;font-style:italic"># -h接ip -p接端口</span>
redis-cli -h 127.0.0.1 -p <span style="color:#0000cf;font-weight:bold">6379</span>

<span style="color:#8f5902;font-style:italic"># 或者直接执行命令</span>
redis-cli -h 127.0.0.1 -p <span style="color:#0000cf;font-weight:bold">6379</span> get hello</code></pre></div>
<h3 id="redis数据结构及实现">Redis数据结构及实现</h3>

<p>Redis的数据结构分别是：
string（字符串） 、 hash（哈希） 、 list（列表） 、 set（集合） 、 zset（有序集
合）</p>

<p>使用type命令实际返回的就是当前键的数据结构类型</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">127.0.0.1:6379&gt; <span style="color:#204a87">type</span> hello
string</code></pre></div>
<table>
<thead>
<tr>
<th>数据结构</th>
<th align="left">内部编码</th>
</tr>
</thead>

<tbody>
<tr>
<td>string</td>
<td align="left">·int： 8个字节的长整型。<br/>·embstr： 小于等于39个字节的字符串。<br/>·raw： 大于39个字节的字符串。</td>
</tr>

<tr>
<td>hash</td>
<td align="left">·ziplist（压缩列表） ： 当哈希类型元素个数小于hash-max-ziplist-entries配置（默认512个） 、 同时所有值都小于hash-max-ziplist-value配置（默认64字节） 时， Redis会使用ziplist作为哈希的内部实现， ziplist使用更加紧凑的结构实现多个元素的连续存储， 所以在节省内存方面比hashtable更加优秀。<br/><br/>·hashtable（哈希表） ： 当哈希类型无法满足ziplist的条件时， Redis会使用hashtable作为哈希的内部实现， 因为此时ziplist的读写效率会下降， 而hashtable的读写时间复杂度为O（1） 。</td>
</tr>

<tr>
<td>List</td>
<td align="left">·ziplist（压缩列表） ： 当列表的元素个数小于list-max-ziplist-entries配置（默认512个） ， 同时列表中每个元素的值都小于list-max-ziplist-value配置时（默认64字节） ， Redis会选用ziplist来作为列表的内部实现来减少内存的使用。<br/>·linkedlist（链表） ： 当列表类型无法满足ziplist的条件时， Redis会使用linkedlist作为列表的内部实现。</td>
</tr>

<tr>
<td>set</td>
<td align="left">·intset（整数集合） ： 当集合中的元素都是整数且元素个数小于set-maxintset-entries配置（默认512个） 时， Redis会选用intset来作为集合的内部实现， 从而减少内存的使用。<br/>·hashtable（哈希表） ： 当集合类型无法满足intset的条件时， Redis会使用hashtable作为集合的内部实现。</td>
</tr>

<tr>
<td>zset</td>
<td align="left">·ziplist（压缩列表） ： 当有序集合的元素个数小于zset-max-ziplistentries配置（默认128个） ， 同时每个元素的值都小于zset-max-ziplist-value配置（默认64字节） 时， Redis会用ziplist来作为有序集合的内部实现， ziplist可以有效减少内存的使用。<br/>·skiplist（跳跃表） ： 当ziplist条件不满足时， 有序集合会使用skiplist作为内部实现， 因为此时ziplist的读写效率会下降。</td>
</tr>
</tbody>
</table>

<table>
<thead>
<tr>
<th>数据结构</th>
<th>使用场景</th>
</tr>
</thead>

<tbody>
<tr>
<td>string</td>
<td>缓存<br/>计数<br/>共享session</td>
</tr>

<tr>
<td>hash</td>
<td>对象信息存储</td>
</tr>

<tr>
<td>List</td>
<td>队列<br/>列表<br/></td>
</tr>

<tr>
<td>set</td>
<td>标签<br/>推荐等</td>
</tr>

<tr>
<td>zset</td>
<td>排行榜</td>
</tr>
</tbody>
</table>

<p>查看内部编码命令 object encoding</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">127.0.0.1:6379&gt; object encoding hello
<span style="color:#4e9a06">&#34;embstr&#34;</span></code></pre></div>
<h2 id="缓存持久化">缓存持久化</h2>

<h3 id="rdb">RDB</h3>

<p>RDB持久化是把当前进程数据生成快照保存到硬盘，</p>

<p>命令为</p>

<ul>
<li><code>save</code> 阻塞执行，已废弃</li>
<li><code>bgsave</code> 创建子进程执行</li>
</ul>

<p>bgsave执行结果</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">33375:M 24 Sep 16:39:02.580 * Background saving started by pid 34280
34280:C 24 Sep 16:39:02.592 * DB saved on disk
34280:C 24 Sep 16:39:02.593 * RDB: 6 MB of memory used by copy-on-write
33375:M 24 Sep 16:39:02.599 * Background saving terminated with success</code></pre></div>
<p>save执行结果</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">33375:M 24 Sep 16:39:02.599 * Background saving terminated with success
33375:M 24 Sep 16:41:27.320 * DB saved on disk</code></pre></div>
<p>触发机制：</p>

<ul>
<li>手动触发</li>
<li>自动触发

<ul>
<li>1） 使用save相关配置， 如“save m n”。 表示m秒内数据集存在n次修改时， 自动触发bgsave。</li>
<li>2） 如果从节点执行全量复制操作， 主节点自动执行bgsave生成RDB文件并发送给从节点。</li>
<li>3） 执行debug reload命令重新加载Redis时， 也会自动触发save操作。</li>
<li>4） 默认情况下执行shutdown命令时， 如果没有开启AOF持久化功能则自动执行bgsave。</li>
</ul></li>
</ul>

<p>bgsave运行流程</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632473860585-20210924165740.png" width = 500 /> </div>

<p>文件压缩</p>

<p>Redis默认采用LZF算法对生成的RDB文件做压缩处理， 压缩后的文件远远小于内存大小， 默认开启， 可以通过参数config setrdbcompression{yes|no}动态修改。虽然压缩RDB会消耗CPU， 但可大幅降低文件的体积， 方便保存到硬盘或通过网络发送给从节点， 因此线上建议开启。</p>

<p>RDB优缺点：</p>

<table>
<thead>
<tr>
<th align="left">优点</th>
<th>缺点</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">文件小，适于全量备份，便于传输和恢复<br/>Redis加载恢复速度快</td>
<td>bgsave创建子进程操作较重，不能实时/秒级持久化，频繁执行成本过高<br/>RDB二进制文件新老版本可能会不兼容</td>
</tr>
</tbody>
</table>

<h3 id="aof">AOF</h3>

<p>记录每次写命令到AOF文件中，恢复时重新执行AOF文件中的命令</p>

<table>
<thead>
<tr>
<th>策略</th>
<th>说明</th>
</tr>
</thead>

<tbody>
<tr>
<td>always</td>
<td>命令写入aof_buf后调用fsync操作同步到AOF文件，每次写入都会执行，不建议</td>
</tr>

<tr>
<td>everysec</td>
<td>命令写入aof_buf后调用系统write操作，write完成后线程返回，fsync同步文件操作由专门线程每秒调用一次。是默认的也是建议的同步策略，兼顾性能和数据安全。理论上宕机只丢1秒数据。</td>
</tr>

<tr>
<td>no</td>
<td>命令写入aof_buf后调用write操作，由操作系统负责控制写入AOF，同步周期最长30秒，性能会提升，但安全无法保证。</td>
</tr>
</tbody>
</table>

<p><strong>系统调用write和fsync说明：</strong></p>

<ul>
<li><p>write操作会触发延迟写（delayed write） 机制。 Linux在内核提供页缓冲区用来提高硬盘IO性能。 write操作在写入系统缓冲区后直接返回。 同步硬盘操作依赖于系统调度机制， 例如： 缓冲区页空间写满或达到特定时间周期。 同步文件之前， 如果此时系统故障宕机， 缓冲区内数据将丢失。</p></li>

<li><p>fsync针对单个文件操作（比如AOF文件） ， 做强制硬盘同步， fsync将阻塞直到写入硬盘完成后返回， 保证了数据持久化。</p></li>
</ul>

<p><strong>重写机制</strong></p>

<p>为避免AOF文件越写越大，Redis引入AOF重写机制压缩文件体积。 AOF文件重写是把Redis进程内的数据转
化为写命令同步到新AOF文件的过程。</p>

<p>文件变小原理：</p>

<ol>
<li>进程内超时数据不再写入文件</li>
<li>删除无效命令如:del等，只使用进程内数据直接生成，保留最终写入命令</li>
<li>集合多次操作命令合并，以64个元素为界拆分为多条</li>
</ol>

<p>触发：</p>

<ul>
<li>手动： 调用bgrewriteaof命令。</li>
<li>自动：根据参数确定自动触发时机。

<ul>
<li>auto-aof-rewrite-min-size: AOF重写最小体积</li>
<li>auto-aof-rewrite-percentage： 代表当前AOF文件空间（ aof_current_size） 和上一次重写后AOF文件空间（ aof_base_size） 的比值。</li>
</ul></li>
</ul>

<p>自动触发时机=aof_current_size&gt;auto-aof-rewrite-min-size&amp;&amp;（ aof_current_size-aof_base_size）/aof_base_size&gt;=auto-aof-rewrite-percentage
其中aof_current_size和aof_base_size可以在info Persistence统计信息中查看。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">127.0.0.1:6380&gt; info Persistence
<span style="color:#8f5902;font-style:italic"># Persistence</span>
loading:0
rdb_changes_since_last_save:2
rdb_bgsave_in_progress:0
rdb_last_save_time:1632476935
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
aof_enabled:1
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:0
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_current_size:199
aof_base_size:199
aof_pending_rewrite:0
aof_buffer_length:0
aof_rewrite_buffer_length:0
aof_pending_bio_fsync:0
aof_delayed_fsync:0</code></pre></div>
<p>aof重写流程</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632479984947-20210924183944.png" width = 400 /> </div>

<h3 id="持久化文件加载流程">持久化文件加载流程</h3>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632480095856-20210924184135.png" width = 400 /> </div>

<p>Redis单线程架构导致无法充分利用CPU多核特性， 通常的做法是在一台机器上部署多个Redis实例。 当多个实例开启AOF重写后， 彼此之间会产生对CPU和IO的竞争。 如果同一时刻运行多个子进程， 对当前系统影响将非常明显， 因此单机下部署多实例时需要采用一种措施， 把子进程工作进行隔离。通过<code>info Persistence</code>命令可以获取监控子进程运行状况的度量指标。基于以下这些指标，可以通过外部程序轮询控制AOF重写操作的串行执行。</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632623914351-20210926103833.png" width = 600 /> </div>

<h2 id="redis特性">Redis特性</h2>

<h3 id="redis单线程为什么快">Redis单线程为什么快？</h3>

<p>第一， 纯内存访问， Redis将所有数据放在内存中， 内存的响应时长大约为100纳秒， 这是Redis达到每秒万级别访问的重要基础。</p>

<p>第二， 非阻塞I/O， Redis使用epoll作为I/O多路复用技术的实现， 再加上Redis自身的事件处理模型将epoll中的连接、 读写、 关闭都转换为事件， 不在网络I/O上浪费过多的时间。</p>

<blockquote>
<p>I/O多路复用实际上就是用select, poll, epoll监听多个io对象，当io对象有变化（有数据）的时候就通知用户进程。好处就是单个进程可以处理多个socket。当然具体区别我们后面再讨论，现在先来看下I/O多路复用的流程：</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632464804122-20210924142643.png" width = 600 /> </div>

<p>　　（1）当用户进程调用了select，那么整个进程会被block；</p>

<p>​        （2）而同时，kernel会“监视”所有select负责的socket；</p>

<p>　　（3）当任何一个socket中的数据准备好了，select就会返回；</p>

<p>　　（4）这个时候用户进程再调用read操作，将数据从kernel拷贝到用户进程。</p>

<p>　　所以，I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符，而这些文件描述符（套接字描述符）其中的<strong>任意一个进入读就绪状态</strong>，select()<strong>函数就可以返回</strong>。</p>

<p>　　这个图和blocking IO的图其实并没有太大的不同，事实上，还更差一些。因为这里需要使用两个system call (select 和 recvfrom)，而blocking IO只调用了一个system call (recvfrom)。但是，用select的优势在于它可以同时处理多个connection。</p>

<p>　　所以，<strong>如果处理的连接数不是很高的话</strong>，使用<strong>select/epoll的web server不一定</strong>比使用<strong>多线程 + 阻塞 IO</strong>的web server性能更好，可能延迟还更大。</p>

<p>　　select/epoll的优势<strong>并不是</strong>对于单个连接能处理得<strong>更快</strong>，而是在于能处理<strong>更多</strong>的连接。）</p>

<p>　　在IO multiplexing Model中，实际中，对于每一个socket，一般都设置成为non-blocking，但是，如上图所示，整个用户的process其实是一直被block的。只不过process是被select这个函数block，而不是被socket IO给block。</p>
</blockquote>

<p>第三， 单线程避免了线程切换和竞态产生的消耗。</p>

<h3 id="慢查询">慢查询</h3>

<p>配置 slowlog-log-slower-than和slowlog-max-len配置，慢查询存储在一个列表中。</p>

<p><code>slowlog-log-slower-than</code>: 为预设阀值，它的单位是微秒（1秒=1000毫秒=1000000微秒） ， 默认值是10000。如果slowlog-log-slower-than=0会记录所有的命令， slowlog-log-slower-than&lt;0对于任何命令都不会进行记录。</p>

<p><code>slowlog-max-len</code>：就是慢查询命令列表的最大长度。</p>

<p>配置方式</p>

<ul>
<li><p>配置文件</p></li>

<li><p>命令</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">config <span style="color:#204a87">set</span> slowlog-log-slower-than <span style="color:#0000cf;font-weight:bold">20000</span>
config <span style="color:#204a87">set</span> slowlog-max-len <span style="color:#0000cf;font-weight:bold">1000</span>
config rewrite</code></pre></div></li>
</ul>

<p>查询慢日志</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#8f5902;font-style:italic"># n为获取慢日志的条数</span>
slowlog get <span style="color:#ce5c00;font-weight:bold">[</span>n<span style="color:#ce5c00;font-weight:bold">]</span>

如：
127.0.0.1:6380&gt; slowlog get <span style="color:#0000cf;font-weight:bold">1</span>
1<span style="color:#ce5c00;font-weight:bold">)</span> 1<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#ce5c00;font-weight:bold">(</span>integer<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#0000cf;font-weight:bold">2016</span>
   2<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#ce5c00;font-weight:bold">(</span>integer<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#0000cf;font-weight:bold">1632467923</span>
   3<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#ce5c00;font-weight:bold">(</span>integer<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#0000cf;font-weight:bold">10</span>
   4<span style="color:#ce5c00;font-weight:bold">)</span> 1<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#4e9a06">&#34;REPLCONF&#34;</span>
      2<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#4e9a06">&#34;ACK&#34;</span>
      3<span style="color:#ce5c00;font-weight:bold">)</span> <span style="color:#4e9a06">&#34;4913377&#34;</span></code></pre></div>
<p>每个慢查询日志有4个属性组成， 分别是慢查询日志的标识id、 发生时间戳、 命令耗时、 执行命令和参数。</p>

<h2 id="redis主从复制">Redis主从复制</h2>

<h3 id="手动将从节点设置为主节点">手动将从节点设置为主节点</h3>

<p>手动将从节点设置成主节点。命令：</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#8f5902;font-style:italic"># redis-cli -h &lt;主节点ip&gt; -p &lt;主节点端口号&gt; slaveof [host] [port]</span>
127.0.0.1:6380&gt; slaveof 127.0.0.1 <span style="color:#0000cf;font-weight:bold">6379</span>
OK</code></pre></div>
<p>配置复制的方式：</p>

<p>1）在配置文件中加入slaveof {masterHost} {masterPort}随Redis启动生效。
2）在redis-server启动命令后加入&ndash;slaveof {masterHost} {masterPort}生效。
3）直接使用命令：slaveof {masterHost} {masterPort}生效。</p>

<p>主从节点复制成功建立后， 可以使用info replication命令查看复制相关状态</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">127.0.0.1:6380&gt; info replication
<span style="color:#8f5902;font-style:italic"># Replication</span>
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:5946163
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:5541</code></pre></div>
<p>从节点执行slaveof no one命令断开与主节点的复制关系，</p>

<p>断开复制主要流程：
1） 断开与主节点复制关系。
2） 从节点晋升为主节点</p>

<p>从节点断开复制后并不会抛弃原有数据， 只是无法再获取主节点上的数据变化。</p>

<p>但是切主后从节点会清空之前所有的数据， 线上人工操作时小心slaveof在错误的节点上执行或者指向错误的主节点。</p>

<p>Redis复制的拓扑结构，分为一主一从，一主多从，树状主从结构。</p>

<p>一主一从：</p>

<p>当应用写命令并发量较高且需要持久化时， 可以只在从节点上开启AOF， 这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是， 当主节点关闭持久化功能时，如果主节点脱机要避免自动重启操作。 因为主节点之前没有开启持久化功能自动重启后数据集为空， 这时从节点如果继续复制主节点会导致从节点数据也被清空的情况， 丧失了持久化的意义。 安全的做法是在从节点上执行slaveof no one断开与主节点的复制关系， 再重启主节点从而避免这一问题。</p>

<p>一主多从：</p>

<p>又叫星形拓扑结构</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632642145495-20210926154225.png" width = 400 /> </div>

<p>树状拓扑：</p>

<p>树状主从结构（又称为树状拓扑结构） 使得从节点不但可以复制主节点数据， 同时可以作为其他从节点的主节点继续向下层复制。 通过引入复制中间层， 可以有效降低主节点负载和需要传送给从节点的数据量。数据实现了一层一层的向下复制。 当主节点需要挂载多个从节点时为了避免对主节点的性能干扰， 可以采用树状主从结构降低主节点压力。</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632642183844-20210926154303.png" width = 400 /> </div>

<h3 id="开发与运维中的问题">开发与运维中的问题</h3>

<h4 id="读写分离方面的问题">读写分离方面的问题</h4>

<p>1）复制数据延迟</p>

<p>Redis复制数据的延迟由于异步复制特性是无法避免的， 延迟取决于网络带宽和命令阻塞情况， 比如刚在主节点写入数据后立刻在从节点上读取可能获取不到。 需要业务场景允许短时间内的数据延迟。 对于无法容忍大量延
迟场景， 可以编写外部监控程序监听主从节点的复制偏移量， 当延迟较大时触发报警或者通知客户端避免读取延迟过高的从节点，</p>

<p>实现逻辑说明：</p>

<ol>
<li>监控程序（monitor） 定期检查主从节点的偏移量， 主节点偏移量在info replication的master_repl_offset指标记录， 从节点偏移量可以查询主节点的slave0字段的offset指标， 它们的差值就是主从节点延迟的字节量。</li>
<li>当延迟字节量过高时， 比如超过10MB。 监控程序触发报警并通知客户端从节点延迟过高。 可以采用Zookeeper的监听回调机制实现客户端通知。</li>
<li>客户端接到具体的从节点高延迟通知后， 修改读命令路由到其他从节点或主节点上。 当延迟恢复后， 再次通知客户端， 恢复从节点的读命令请求。</li>
</ol>

<p>这种方案的成本比较高， 需要单独修改适配Redis的客户端类库。 如果涉及多种语言成本将会扩大。 客户端逻辑需要识别出读写请求并自动路由，还需要维护故障和恢复的通知。 采用此方案视具体的业务而定， 如果允许不一致性或对延迟不敏感的业务可以忽略， 也可以采用Redis集群方案做水平扩展。</p>

<p>2）读到过期数据</p>

<p>当主节点存储大量设置超时的数据时。</p>

<p>Redis在3.2版本解决了这个问题， 从节点读取数据之前会检查键的过期时间来决定是否返回数据， 可以升级到3.2版本来规避这个问题。</p>

<p>3）从节点故障</p>

<p>对于从节点的故障问题， 需要在客户端维护可用从节点列表， 当从节点故障时立刻切换到其他从节点或主节点上。 这个过程类似上文提到的针对延迟过高的监控处理， 需要开发人员改造客户端类库。</p>

<p>总结：使用Redis做读写分离存在一定的成本。 Redis本身的性能非常高， 开发人员在使用额外的从节点提升读性能之前， 尽量在主节点上做充分优化， 比如解决慢查询， 持久化阻塞， 合理应用数据结构等， 当主节点优
化空间不大时再考虑扩展。 笔者建议大家<strong>在做读写分离之前， 可以考虑使用Redis Cluster等分布式解决方案， 这样不止扩展了读性能还可以扩展写性能和可支撑数据规模， 并且一致性和故障转移也可以得到保证， 对于客户端的维护逻辑也相对容易</strong>。</p>

<h2 id="内存相关">内存相关</h2>

<h3 id="内存使用统计">内存使用统计</h3>

<p>使用<code>info memory</code>命令</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632647204130-20210926170643.png" width = 600 /> </div>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">127.0.0.1:6380&gt; info memory
# Memory
used_memory:841672
used_memory_human:821.95K
used_memory_rss:9965568
used_memory_rss_human:9.50M
used_memory_peak:1869392
used_memory_peak_human:1.78M
total_system_memory:1019572224
total_system_memory_human:972.34M
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:11.84
mem_allocator:jemalloc-4.0.3</code></pre></div>
<p>重点指标</p>

<ul>
<li>used_memory_rss:9965568
used_memory_rss_human:9.50M</li>
</ul>

<p>从操作系统角度显示Redis进行战胜的物理内在总量</p>

<ul>
<li>used_memory:841672
used_memory_human:821.95K</li>
</ul>

<p>Redis分配器分配的内存总量，也就是内部存储所有的数内存占用量</p>

<ul>
<li>mem_fragmentation_ratio:11.84</li>
</ul>

<p>mem_fragmentation_ratio是used_memory_rss和used_memory的比值，</p>

<p>当mem_fragmentation_ratio&gt;1时， 说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储， 而是被内存碎片所消耗， 如果两者相差很大， 说明碎片率严重。
  当mem_fragmentation_ratio&lt;1时， 这种情况一般出现在操作系统把Redis内存交换（Swap） 到硬盘导致， 出现这种情况时要格外关注， 由于硬盘速度远远慢于内存， Redis性能会变得很差， 甚至僵死。</p>

<h3 id="内存消耗划分">内存消耗划分</h3>

<p>Redis进程内消耗主要包括： 自身内存+对象内存+缓冲内存+内存碎片</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632647992408-20210926171951.png" width = 300 /> </div>

<h4 id="对象内存">对象内存</h4>

<p>对象内存是Redis内存占用最大的一块， 存储着用户所有的数据。</p>

<h4 id="缓冲内存">缓冲内存</h4>

<p>客户端缓冲区</p>

<p>指所有接入到Redis服务器TCP连接的输入输出缓冲，包括</p>

<ul>
<li>普通客户端</li>
</ul>

<p>一般普通客户端的内存消耗可以忽略不计， 但是当有大量慢连接客户端接入时这部分内存消耗就不能忽略了， 可以设置maxclients做限制。 特别是当使用大量数据输出的命令且数据无法及时推送给客户端时，如monitor命令， 容易造成Redis服务器内存突然飙升。</p>

<ul>
<li>从客户端</li>
</ul>

<p>当主从节点之间网络延迟较高或主节点挂载大量从节点时这部分内存消耗将占用很大一部分， 建议主节点挂载的从节点不要多于2个， 主从节点不要部署在较差的网络环境下， 如异地跨机房环境， 防止复制客户端连接缓慢造成溢出。</p>

<ul>
<li>订阅客户端</li>
</ul>

<p>当使用发布订阅功能时， 连接客户端使用单独的输出缓冲区，当订阅服务的消息生产快于消费速度时， 输出缓冲区会产生积压造成输出缓冲区空间溢出。</p>

<p>复制积压缓冲区</p>

<p>Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能，</p>

<p>AOF缓冲区</p>

<p>这部分空间用于在Redis重写期间保存最近的写入命令，AOF缓冲区空间消耗用户无法控制， 消耗的内存取决于
AOF重写时间和写入命令量， 这部分空间占用通常很小。</p>

<h4 id="内存碎片">内存碎片</h4>

<p>jemalloc针对碎片化问题专门做了优化， 一般不会存在过度碎片化的问题， 正常的碎片率（ mem_fragmentation_ratio） 在1.03左右。 但是当存储的数据长短差异较大时， 以下场景容易出现高内存碎片问题：</p>

<ul>
<li>频繁做更新操作， 例如频繁对已存在的键执行append、 setrange等更新
操作。</li>
<li>大量过期键删除， 键对象过期删除后， 释放的空间无法得到充分利
用， 导致碎片率上升。</li>
</ul>

<p>出现高内存碎片问题时常见的解决方式如下：</p>

<ul>
<li>数据对齐： 在条件允许的情况下尽量做数据对齐， 比如数据尽量采用数字类型或者固定长度字符串等， 但是这要视具体的业务而定， 有些场景无法做到。</li>
<li>安全重启： 重启节点可以做到内存碎片重新整理， 因此可以利用高可用架构， 如Sentinel或Cluster， 将碎片率过高的主节点转换为从节点， 进行安全重启。</li>
</ul>

<h3 id="子进程内存消耗">子进程内存消耗</h3>

<p>子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。</p>

<p>Linux具有写时复制技术（copy-on-write）， 父子进程会共享相同的物理内存页，所以不需要多一倍的物理内存，当父进程处理写请求时会对需要修改的页复制出一份副本完成写操作， 而子进程依然读取fork时整个父进程的内存快照。</p>

<p>Linux Kernel在2.6.38内核增加了Transparent Huge Pages（THP） 机制，然开启THP可以降低fork子进程的速度， 但之后copy-on-write期间复制内存页的单位从4KB变为2MB， 如果父进程有大量写命令， 会加重内存拷贝量， 从而造成过度内存消耗。</p>

<p>建议：</p>

<ul>
<li>Redis产生的子进程并不需要消耗1倍的父进程内存， 实际消耗根据期间写入命令量决定， 但是依然要预留出一些内存防止溢出。</li>
<li>需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存， 防止Redis进程执行fork时因系统剩余内存不足而失败。</li>
<li>排查当前系统是否支持并开启THP， 如果开启建议关闭， 防止copy-onwrite期间内存过度消耗。</li>
</ul>

<h3 id="内存管理">内存管理</h3>

<h4 id="设置内存上限">设置内存上限</h4>

<p>使用maxmemory参数限制最大可用内存。maxmemory限制的是Redis实际使用的内存量， 也就是used_memory统计项对应的内存。由于内存碎片率的存在， 实际消耗的内存可能会比maxmemory设置的更大， 实际使用时要小心这部分内存溢出。</p>

<p>目的：</p>

<ul>
<li>缓存场景，内存超出maxmemory时使用LRU等删除策略释放空间。</li>
<li>防止内存超过服务器物理内存。</li>
</ul>

<p>Redis默认无限使用服务器内存， 为防止极端情况下导致系统内存耗尽， 建议所有的Redis进程都要配置maxmemory。</p>

<h4 id="动态调整内存上限">动态调整内存上限</h4>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">127.0.0.1:6379&gt; config set maxmemory 100mb
OK
127.0.0.1:6379&gt; info memory
# Memory
maxmemory:104857600
maxmemory_human:100.00M</code></pre></div>
<p>在保证物理内存可用的情况下， 系统中所有Redis实例可以调整maxmemory参数来达到自由伸缩内存的目的。</p>

<h4 id="内存回收策略">内存回收策略</h4>

<h5 id="删除过期键对象">删除过期键对象</h5>

<ul>
<li>惰性删除</li>
</ul>

<p>当客户端读取带有超时属性的键时， 如果已经超过键设置的过期时间， 会执行删除操作并返回空。但是单独用这种方式存在内存泄露的问题。</p>

<ul>
<li>定时删除</li>
</ul>

<p>1） 定时任务在每个数据库空间随机检查20个键， 当发现过期时删除对应的键。</p>

<p>2） 如果超过检查数25%的键过期， 循环执行回收逻辑直到不足25%或运行超时为止， 慢模式下超时时间为25毫秒。
  3） 如果之前回收键逻辑超时， 则在Redis触发内部事件之前再次以快模式运行回收过期键任务， 快模式下超时时间为1毫秒且2秒内只能运行1次。
  4） 快慢两种模式内部删除逻辑相同， 只是执行的超时时间不同。</p>

<p><div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632653445070-20210926185044.png" width = 400 /> </div></p>

<h5 id="内存溢出控制策略">内存溢出控制策略</h5>

<p>内存使用到达maxmemory上限触发</p>

<p>1） noeviction： 默认策略， 不会删除任何数据， 拒绝所有写入操作并返回客户端错误信息（ error） OOM command not allowed when used memory， 此时Redis只响应读操作。
2） volatile-lru： 根据LRU算法删除设置了超时属性（ expire） 的键， 直到腾出足够空间为止。 如果没有可删除的键对象， 回退到noeviction策略。
3） allkeys-lru： 根据LRU算法删除键， 不管数据有没有设置超时属性，直到腾出足够空间为止。
4） allkeys-random： 随机删除所有键， 直到腾出足够空间为止。
5） volatile-random： 随机删除过期键， 直到腾出足够空间为止。
6） volatile-ttl： 根据键值对象的ttl属性， 删除最近将要过期数据。 如果没有， 回退到noeviction策略。</p>

<p>内存溢出控制策略可以采用config set maxmemory-policy {policy}动态配置。</p>

<p>每次Redis执行命令时如果设置了maxmemory参数， 都会尝试执行回收内存操作。 当Redis一直工作在内存溢出（used_memory&gt;maxmemory） 的状态下且设置非noeviction策略时， 会频繁地触发回收内存的操作， 影响Redis服务器的性能。频繁执行回收内存成本很高， 主要包括查找可回收键和删除键的开销， 如果当前Redis有从节点， 回收内存操作对应的删除命令会同步到从节点， 导致写放大的问题。</p>

<p>线上建议：</p>

<p>建议线上Redis内存工作在maxmemory&gt;used_memory状态下， 避免频繁内存回收开销。</p>

<p>对于需要收缩Redis内存的场景， 可以通过调小maxmemory来实现快速回收。注意， 此操作会导致数据丢失
和短暂的阻塞问题， 一般在缓存场景下使用。</p>

<h3 id="内存优化">内存优化</h3>

<ul>
<li><p>可以使用scan+object idletime命令批量查询哪些键长时间未被访问， 找出长时间不访问的键进行清理， 可降低内存占用。</p></li>

<li><p>高并发写入场景中， 在条件允许的情况下， 建议字符串长度控制在39字节以内， 减少创建redisObject内存分配次数， 从而提高性能。</p></li>

<li><p>优化字符串：尽量减少字符串频繁修改操作如append、 setrange， 改为直接使用set修改字符串， 降低预分配带来的内存浪费和内存碎片化。</p></li>
</ul>

<p>内存优化的思路概括：</p>

<ul>
<li>精简键值对大小， 键值字面量精简， 使用高效二进制序列化工具。</li>
<li>使用对象共享池优化小整数对象。</li>
<li>数据优先使用整数， 比字符串类型更节省空间。</li>
<li>优化字符串使用， 避免预分配造成的内存浪费。</li>
<li>使用ziplist压缩编码优化hash、 list等结构， 注重效率和空间的平衡。</li>
<li>使用intset编码优化整数集合。</li>
<li>使用ziplist编码的hash结构降低小对象链规模</li>
</ul>

<h2 id="哨兵">哨兵</h2>

<p>Redis Sentinel是一个分布式架构， 其中包含若干个Sentinel节点和Redis数据节点， 每个Sentinel节点会对数据节点和其余Sentinel节点进行监控， 当它发现节点不可达时， 会对节点做下线标识。 如果被标识的是主节点， 它还会和其他Sentinel节点进行“协商”， 当大多数Sentinel节点都认为主节点不可达时， 它们会选举出一个Sentinel节点来完成自动故障转移的工作， 同时会将这个变化实时通知给Redis应用方。 整个过程完全是自动的， 不需要人工来介入， 所以这套方案很有效地解决了Redis的高可用问题。</p>

<p>Redis2.6版本提供Redis Sentinel v1版本， 但是功能性和健壮性都有一些问题， 如果想使用Redis Sentinel的话， 建议使用2.8以上版本， 也就是v2版本的Redis Sentinel。</p>

<p>sentinel配置参数</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">redis-sentinel-26379.conf
port 26379
daemonize yes
logfile &#34;26379.log&#34;
dir /opt/soft/redis/data
#                master节点名称 host 端口 sentinel节点协商通过数量
sentinel monitor mymaster 127.0.0.1 6379 2
#        判定节点不可达时间，单位是毫秒
sentinel down-after-milliseconds mymaster 30000
#        parallel-syncs就是用来限制在一次故障转移之后， 每次向新的主节点发起复制操作的从节点个数。
sentinel parallel-syncs mymaster 1
# 			 故障转移超时时间
sentinel failover-timeout mymaster 180000</code></pre></div>
<p>说明：</p>

<ul>
<li><p>down-after-milliseconds越大， 代表Sentinel节点对于节点不可达的条件越宽松， 反之越严格。 条件宽松有可能带来的问题是节点确实不可达了， 那么应用方需要等待故障转移的时间越长， 也就意味着应用方故障时间可能越长。 条件严格虽然可以及时发现故障完成故障转移， 但是也存在一定的误判率。</p></li>

<li><p>failover-timeout通常被解释成故障转移超时时间， 但实际上它作用于故障转移的各个阶段：
a） 选出合适从节点。
b） 晋升选出的从节点为主节点。
c） 命令其余从节点复制新的主节点。
d） 等待原主节点恢复后命令它去复制新的主节点。</p></li>
</ul>

<p>failover-timeout的作用具体体现在四个方面：
  1） 如果Redis Sentinel对一个主节点故障转移失败， 那么下次再对该主节点做故障转移的起始时间是failover-timeout的2倍。
  2） 在b） 阶段时， 如果Sentinel节点向a） 阶段选出来的从节点执行slaveof no one一直失败（例如该从节点此时出现故障） ， 当此过程超过failover-timeout时， 则故障转移失败。
  3） 在b） 阶段如果执行成功， Sentinel节点还会执行info命令来确认a）阶段选出来的节点确实晋升为主节点， 如果此过程执行时间超过failovertimeout时， 则故障转移失败。
  4） 如果c） 阶段执行时间超过了failover-timeout（不包含复制时间），则故障转移失败。 注意即使超过了这个时间， Sentinel节点也会最终配置从节点去同步最新的主节点。</p>

<p>启动命令，两种结果相同</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">redis-sentinel redis-sentinel-26379.conf
或
redis-server redis-sentinel-26379.conf --sentinel</code></pre></div>
<p>要注意sentinel配置文件在应用重启以后会被重写，所以要保留原始配置内容</p>

<p>sentinel启动后可以使用<code>info sentinel</code>命令查看相关信息。</p>

<p>1） 生产环境中建议Redis Sentinel的所有节点应该分布在不同的物理机上。
2） Redis Sentinel中的数据节点和普通的Redis数据节点在配置上没有任何区别， 只不过是添加了一些Sentinel节点对它们进行监控。</p>

<p>sentinel拓扑结构：</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/1632657625308-20210926200024.png" width = 600 /> </div>

<p>部署技巧：</p>

<p>1） Sentinel节点不应该部署在一台物理“机器”上。</p>

<p>2） 部署至少三个且奇数个的Sentinel节点</p>

<p>3） 是只用一套Sentinel， 还是每个主节点配置一套Sentinel？</p>

<p>如果Sentinel节点集合监控的是同一个业务的多个主节点集合， 那么使用方案一、 否则一般建议采用方案二。</p>

<h2 id="集群">集群</h2>

<p>Redis Cluster是Redis的分布式解决方案， 在3.0版本正式推出， 有效地解决了Redis分布式方面的需求。 当遇到单机内存、 并发、 流量等瓶颈时， 可以采用Cluster架构方案达到负载均衡的目的。</p>

<p>分区</p>

<p>RedisCluster采取的是虚拟槽分区的方式进行分区。</p>

<table>
<thead>
<tr>
<th></th>
<th>节点取余</th>
<th>一致性hash</th>
<th>虚拟槽分区</th>
</tr>
</thead>

<tbody>
<tr>
<td>原理</td>
<td>使用特定的数据， 如Redis的键，根据节点数量N使用公式：<br/>hash（key） %N计算出哈希值， 用来决定数据映射到哪一个节点上。</td>
<td>实现思路是为系统中每个节<br/>点分配一个token， 范围一般在0~232， 这些token构成一个哈希环。 数据读写<br/>执行节点查找操作时， 先根据key计算hash值， 然后顺时针找到第一个大于<br/>等于该哈希值的token节点， 如图10-3所示。</td>
<td>使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中， 整数定义为槽（slot） 。 这个范围一般远远大于节点数， 比如Redis Cluster槽范围是0~16383，使用CRC16（key） &amp;16383将键映射到槽上。</td>
</tr>

<tr>
<td>优点</td>
<td>简单，一般采<br/>用预分区的方式， 提前根据数据量规划好分区数，保证可支撑未来一段时间的数据量， 再根据负载情况将表迁移到其他数<br/>据库中</td>
<td>这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中<br/>相邻的节点， 对其他节点无影响</td>
<td>解耦数据和节点之间的关系， 简化了节点扩容和收缩难度。<br/>节点自身维护槽的映射关系， 不需要客户端或者代理服务维护槽分区元数据。<br/>支持节点、 槽、 键之间的映射查询， 用于数据路由、 在线伸缩等场景。</td>
</tr>

<tr>
<td>缺点</td>
<td>扩容时通常采用翻倍扩容， 避免数据映射全部被打乱导致全量迁移<br/>的情况</td>
<td>·加减节点会造成哈希环中部分数据无法命中， 需要手动处理或者忽略这部分数据， 因此一致性哈希常用于缓存场景。<br/>·当使用少量节点时， 节点变化将大范围影响哈希环中数据映射， 因此这种方式不适合少量数据节点的分布式方案。<br/>普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡</td>
<td></td>
</tr>
</tbody>
</table>

<p>集群功能限制：</p>

<p>1） key批量操作支持有限。 如mset、 mget， 目前只支持具有相同slot值的key执行批量操作。 对于映射为不同slot值的key由于执行mget、 mget等操作可能存在于多个节点上因此不被支持。
2） key事务操作支持有限。 同理只支持多key在同一节点上的事务操作， 当多个key分布在不同的节点上时无法使用事务功能。
3） key作为数据分区的最小粒度， 因此不能将一个大的键值对象如hash、 list等映射到不同的节点。
4） 不支持多数据库空间。 单机下的Redis可以支持16个数据库， 集群模式下只能使用一个数据库空间， 即db0。
5） 复制结构只支持一层， 从节点只能复制主节点， 不支持嵌套树状复制结构。</p>

<p>集群配置文件</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">#节点端口
port 6379
# 开启集群模式
cluster-enabled yes
# 节点超时时间， 单位毫秒
cluster-node-timeout 15000
# 集群内部配置文件
cluster-config-file &#34;nodes-6379.conf&#34;</code></pre></div>
<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/20211011183425.png" width = 300 /> </div>

<p>集群模式的Redis除了原有的配置文件之外又加了一份集群配置文件。当集群内节点信息发生变化， 如添加节点、 节点下线、 故障转移等。 节点会自动保存集群状态到配置文件中。 需要注意的是， Redis自动维护集群配置文件， 不要手动修改， 防止节点重启时产生集群信息错乱。</p>

<p>集群启动流程</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">graph 

A((启动节点)) --&gt; B[节点握手]

B --&gt; C[分配槽]

C --&gt; D((集群在线))</code></pre></div>
<p>集群伸缩</p>

<ul>
<li><p>集群扩容</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">准备节点-&gt; 加入集群 -&gt; 迁移槽</code></pre></div></li>
</ul>

<p><font color=red> PS:正式环境建议使用redis-trib.rb add-node命令加入新节点， 该命令内部会
  执行新节点状态检查， 如果新节点已经加入其他集群或者包含数据， 则放弃
  集群加入操作</font></p>

<p><font color=red> 使用redis-trib.rb reshard命令迁移槽</font></p>

<ul>
<li><p>集群缩容</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">迁移槽-&gt;下线节点</code></pre></div></li>
</ul>

<p>使用redis-trib.rb reshard命令迁移槽到其他节点</p>

<p>忘记节点  <code>cluster forget {downNodeId}</code></p>

<p><font color=red>线上操作不建议直接使用cluster forget命令下线节点， 需要跟大量节点命令交互， 实际操作起来过于繁琐并且容易遗漏forget节点。 建议使用redistrib.rb del-node {host： port} {downNodeId}命令</font></p>

<p>当下线主节点具有从节点时需要把该从节点指向到其他主节点， 因此对于主从节点都下线的情况， 建议先下线从节点再下线主节点， 防止不必要的全量复制。</p>

<p>在集群模式下， Redis接收任何键相关命令时首先计算键对应的槽， 再根据槽找出所对应的节点， 如果节点是自身， 则处理键命令； 否则回复MOVED重定向错误， 通知客户端请求正确的节点。 这个过程称为MOVED重
定向。</p>

<div  align="center"><img src="https://wejack639.oss-cn-beijing.aliyuncs.com/blogimages/img/20211012105320.png" width = 400 /> </div>

<p>键内部使用大括号包含的内容又叫做hash_tag， 它提供不同的键可以具备相同slot的功能， 常用于Redis IO优化。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">127.0.0.1:6379&gt; cluster keyslot key:test:111
(integer) 10050
127.0.0.1:6379&gt; cluster keyslot key:{hash_tag}:111
(integer) 2515
127.0.0.1:6379&gt; cluster keyslot key:{hash_tag}:222
(integer) 2515

127.0.0.1:6385&gt; mget user:10086:frends user:10086:videos
(error) CROSSSLOT Keys in request don&#39;t hash to the same slot
127.0.0.1:6385&gt; mget user:{10086}:friends user:{10086}:videos
1) &#34;friends&#34;
2) &#34;videos&#34;</code></pre></div>
<p>Pipeline同样可以受益于hash_tag， 由于Pipeline只能向一个节点批量发送执行命令， 而相同slot必然会对应到唯一的节点， 降低了集群使用Pipeline的门槛</p>

<p>对于JedisCluster的使用需要注意以下几点：</p>

<ul>
<li>JedisCluster包含了所有节点的连接池（ JedisPool） ， 所以建议JedisCluster使用单例。</li>
<li>JedisCluster每次操作完成后， 不需要管理连接池的借还， 它在内部已经完成。</li>
<li>JedisCluster一般不要执行close（ ） 操作， 它会将所有JedisPool执行destroy操作。</li>
</ul>

<p>故障发现与转移</p>

<ul>
<li>主观下线： 指某个节点认为另一个节点不可用， 即下线状态， 这个状态并不是最终的故障判定， 只能代表一个节点的意见， 可能存在误判情况。</li>
</ul>

<p>集群中每个节点都会定期向其他节点发送ping消息， 接收节点回复pong消息作为响应。 如果在cluster-node-timeout时间内通信一直失败， 则发送节点会认为接收节点存在故障， 把接收节点标记为主观下线（pfail） 状态。</p>

<ul>
<li><p>客观下线： 指标记一个节点真正的下线， 集群内多个节点都认为该节点不可用， 从而达成共识的结果。 如果是持有槽的主节点故障， 需要为该节点进行故障转移。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">1） 为什么必须是负责槽的主节点参与故障发现决策？ 因为集群模式下
只有处理槽的主节点才负责读写请求和集群槽等关键信息维护， 而从节点只
进行主节点数据和状态信息的复制。
2） 为什么半数以上处理槽的主节点？ 必须半数以上是为了应对网络分
区等原因造成的集群分割情况， 被分割的小集群因为无法完成从主观下线到
客观下线这一关键过程， 从而防止小集群完成故障转移之后继续对外提供服
务。</code></pre></div></li>
</ul>

<blockquote>
<p>如果在cluster-node-time*2时间内无法收集到一半以上槽节点的下线报
告， 那么之前的下线报告将会过期， 也就是说主观下线上报的速度追赶不上
下线报告过期的速度， 那么故障节点将永远无法被标记为客观下线从而导致
故障转移失败。 因此不建议将cluster-node-time设置得过小。</p>

<p>网络分区会导致分割后的小集群无法收到大集群的fail消息， 因此如果
故障节点所有的从节点都在小集群内将导致无法完成后续故障转移， 因此部
署主从结构时需要根据自身机房/机架拓扑结构， 降低主从被分区的可能
性。</p>
</blockquote>

<p>故障转移时间
在介绍完故障发现和恢复的流程后， 这时我们可以估算出故障转移时间：
1） 主观下线（pfail） 识别时间=cluster-node-timeout。
2） 主观下线状态消息传播时间&lt;=cluster-node-timeout/2。 消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息， 消息体在选择包含哪些节点时会优先选取下线状态节点， 所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
3） 从节点转移时间&lt;=1000毫秒。 由于存在延迟发起选举机制， 偏移量最大的从节点会最多延迟1秒发起选举。 通常第一次选举就会成功， 所以从节点执行转移时间在1秒以内。</p>

<p>根据以上分析可以预估出故障转移时间， 如下：</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">failover-time(毫秒) ≤ cluster-node-timeout + cluster-node-timeout/2 + 1000</code></pre></div>
<p>因此， 故障转移时间跟cluster-node-timeout参数息息相关， 默认15秒。</p>

<p>集群完整性</p>

<p>为了保证集群完整性， 默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。 执行任何键命令返回（error） CLUSTERDOWN Hash slot not served错误。 这是对集群完整性的一种保护措施， 保证所有的槽都指派给在线的节点。 但是当持有槽的主节点下线时， 从故障发现到自动完成转移期间整个集群是不可用状态， 对于大多数业务无法容忍这种情况，因此建议将参数cluster-require-full-coverage配置为no， 当主节点故障时只影响它负责槽的相关命令执行， 不会影响其他主节点的可用性。</p>

<p>集群倾斜</p>

<ol>
<li><p>数据倾斜
数据倾斜主要分为以下几种：</p>

<ul>
<li>节点和槽分配严重不均。</li>
</ul>

<p>针对每个节点分配的槽不均的情况， 可以
 使用redis-trib.rb info {host: ip}进行定位，列举出每个节点负责的槽和键总量以及每个槽平均键数量。 当节点对应槽数量不均匀时， 可以使用redis-trib.rb rebalance命令进行平衡</p>

<ul>
<li>不同槽对应键数量差异过大。</li>
</ul>

<p>键通过CRC16哈希函数映射到槽上，正常情况下槽内键数量会相对均匀。 但当大量使用hash_tag时， 会产生不同的键映射到同一个槽的情况。 特别是选择作为hash_tag的数据离散度较差时， 将加速槽内键数量倾斜情况。 通过命令： cluster  countkeysinslot {slot}可以获取槽对应的键数量， 识别出哪些槽映射了过多的键。 再通过命令cluster getkeysinslot {slot} {count}循环迭代出槽下所有的键。 从而发现过度使用hash_tag的键</p>

<ul>
<li>集合对象包含大量元素。</li>
</ul>

<p><code>redis-cli -p 7002 --bigkeys</code> 查找大key</p>

<ul>
<li>内存相关配置不一致。

<br /></li>
</ul></li>

<li><p>请求倾斜</p></li>
</ol>

<p>当热点键对应高算法复杂度的命令或者是大对象操作如hgetall、 smembers等， 会导致对应节点负载过高的情况。避免方式如下：
   1） 合理设计键， 热点大集合对象做拆分或使用hmget替代hgetall避免整体读取。
   2） 不要使用热键作为hash_tag， 避免映射到同一槽。
   3） 对于一致性要求不高的场景， 客户端可使用本地缓存减少热键调用。</p>

<p>手动故障转移</p>

<p><code>cluster failover</code></p>

<blockquote>
<p>主从节点转移后， 新的从节点由于之前没有缓存主节点信息无法使用部分复制功能， 所以会发起全量复制， 当节点包含大量数据时会严重消耗CPU和网络资源， 线上不要频繁操作。</p>
</blockquote>

<p>手动故障转移的应用场景:
1） 主节点迁移:如节点所在的老机器替换到新机器等。</p>

<p>2） 自动故障转移失效强制转移。</p>

<p>自动故障转移失败的场景有：</p>

<ul>
<li>主节点和它的所有从节点同时故障。 这个问题需要通过调整节点机器部署拓扑做规避， 保证主从节点不在同一机器/机架上。 除非机房内大面积故障， 否则两台机器/机架同时故障概率很低。</li>
<li>所有从节点与主节点复制断线时间超过cluster-slave-validityfactor<em>cluster-node-tineout+repl-ping-slave-period， 导致从节点被判定为没有故障转移资格， 手动故障转移从节点不做中断超时检查。</em></li>
<li>由于网络不稳定等问题， 故障发现或故障选举时间无法在cluster-nodetimeout*2内完成， 流程会不断重试， 最终从节点复制中断时间超时， 失去故障转移资格无法完成转移。</li>
<li>集群内超过一半以上的主节点同时故障。</li>
</ul>

<p>cluster failover命令提供了两个参数force/takeover提供支持</p>

<ul>
<li>cluster failover force——用于当主节点宕机且无法自动完成故障转移情况。 从节点接到cluster failover force请求时， 从节点直接发起选举， 不再跟主节点确认复制偏移量（从节点复制延迟的数据会丢失） ， 当从节点选举成功后替换为新的主节点并广播集群配置。</li>
<li>cluster failover takeover——用于集群内超过一半以上主节点故障的场景， 因为从节点无法收到半数以上主节点投票， 所以无法完成选举过程。 可以执行cluster failover takeover强制转移， 接到命令的从节点不再进行选举流程而是直接更新本地配置纪元并替换主节点。 takeover故障转移由于没有通过领导者选举发起故障转移， 会导致配置纪元存在冲突的可能。 当冲突发生时， 集群会以nodeId字典序更大的一方配置为准。 因此要小心集群分区后，手动执行takeover导致的集群冲突问题。</li>
</ul>

<blockquote>
<p>在集群可以自动完成故障转移的情况下， 不要使用cluster failover takeover强制干扰集群选举机制， 该操作主要用于半数以上主节点故障时采取的强制措施</p>

<p>手动故障转移时， 在满足当前需求的情况下建议优先级： cluster failver&gt;cluster failover force&gt;cluster failover takeover。</p>
</blockquote>

<h2 id="常用命令">常用命令</h2>

<h3 id="集群-1">集群</h3>

<ul>
<li><code>cluster nodes</code></li>
</ul>

<p>查看redis集群内的节点信息</p>

<ul>
<li><code>cluster meet ip port</code></li>
</ul>

<p>节点握手通信，建立集群</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">  如：cluster meet 127.0.0.1 6381</code></pre></div>
<ul>
<li><code>cluster info</code></li>
</ul>

<p>查看集群状态</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">  127.0.0.1:7002&gt; cluster info
  cluster_state:ok
  cluster_slots_assigned:16384
  cluster_slots_ok:16384
  cluster_slots_pfail:0
  cluster_slots_fail:0
  cluster_known_nodes:8
  cluster_size:4
  cluster_current_epoch:5
  cluster_my_epoch:4
  cluster_stats_messages_sent:61587
  cluster_stats_messages_received:61585</code></pre></div>
<ul>
<li><code>redis-cli -h ip -p port cluster addslots {0...5461}</code></li>
</ul>

<p>给节点分配槽</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">  redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0...5461}</code></pre></div>
<ul>
<li><code>cluster replicate {nodeId}</code></li>
</ul>

<p>让一个节点成为从节点，其中命令执行必须在对应的从节点上执行， nodeId是要复制主节点的节点ID。</p>

<ul>
<li><code>redis-trib.rb create --replicas 1 127.0.0.1:6481 127.0.0.1:6482 127.0.0.1:6483
127.0.0.1:6484 127.0.0.1:6485 127.0.0.1:6486</code></li>
</ul>

<p>官方提供的Ruby语言的Redis集群管理工具，自动创建集群</p>

<p>首先要启动每个节点</p>

<p>然后执行命令</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">  #--replicas 1 指定集群中每个主节点配备几个从节点
  redis-trib.rb create --replicas 1 127.0.0.1:6481 127.0.0.1:6482 127.0.0.1:6483
  127.0.0.1:6484 127.0.0.1:6485 127.0.0.1:6486</code></pre></div>
<p><font color=red>这里需要注意给redis-trib.rb的节点地址必须是不包含任何槽/数据的节点， 否则会拒绝创建集群</font></p>

<ul>
<li><code>redis-trib.rb check 127.0.0.1:6379</code></li>
</ul>

<p>集群完整性检查</p>

<ul>
<li><code>redis-trib.rb add-node new_host:new_port existing_host:existing_port --slave
--master-id &lt;arg&gt;</code></li>
</ul>

<p>加入节点到集群</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">  redis-trib.rb add-node 127.0.0.1:6385 127.0.0.1:6379</code></pre></div>
<ul>
<li><div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">redis-trib.rb reshard host:port --from &lt;arg&gt; --to &lt;arg&gt; --slots &lt;arg&gt; --yes --timeout
&lt;arg&gt; --pipeline &lt;arg&gt;</code></pre></div></li>
</ul>

<p>槽迁移命令</p>

<p>参数说明：</p>

<ul>
<li>host：port： 必传参数， 集群内任意节点地址， 用来获取整个集群信息。</li>
<li>&ndash;from： 制定源节点的id， 如果有多个源节点， 使用逗号分隔， 如果是all源节点变为集群内所有主节点， 在迁移过程中提示用户输入。</li>
<li>&ndash;to： 需要迁移的目标节点的id， 目标节点只能填写一个， 在迁移过程中提示用户输入。</li>
<li>&ndash;slots： 需要迁移槽的总数量， 在迁移过程中提示用户输入。</li>
<li>&ndash;yes： 当打印出reshard执行计划时， 是否需要用户输入yes确认后再执行reshard。</li>
<li>&ndash;timeout： 控制每次migrate操作的超时时间， 默认为60000毫秒。</li>

<li><p>&ndash;pipeline： 控制每次批量迁移键的数量， 默认为10。</p></li>

<li><p><code>redistrib.rb del-node {host： port} {downNodeId}</code></p></li>
</ul>

<p>host和port参数都不是下线节点的ip和端口，是其他的</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">  redis-trib.rb del-node 127.0.0.1:6379 4fa7eac4080f0b667ffeab9b87841da49b84a6e4 </code></pre></div>
<ul>
<li><code>cluster keyslot {key}</code></li>
</ul>

<p>命令返回key所对应的槽</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-fallback" data-lang="fallback">  127.0.0.1:7002&gt; cluster keyslot hello
  (integer) 866</code></pre></div>
<ul>
<li><code>redis-cli -p 7002 --bigkeys</code></li>
</ul>

<p>查找大对象</p>

<ul>
<li><code>cluster failover</code></li>
</ul>

<p>手动故障转移</p>

<h3 id="其他">其他</h3>

<ul>
<li><code>type hello</code></li>
</ul>

<p>查看数据类型</p>

<ul>
<li><code>object encoding hello</code></li>
</ul>

<p>查看内部编码</p>

<ul>
<li><code>info</code></li>
</ul>

<p>查看节点信息</p>

    </div>
    <footer class="post-footer">
     

     <div class="post-nav">
    <div class="post-nav-next post-nav-item">
    
        <a href="http://www.shutdown.cn/post/redis%E5%AD%90%E8%BF%9B%E7%A8%8B%E5%BC%80%E9%94%80%E4%B8%8E%E4%BC%98%E5%8C%96/" rel="next" title="">
        <i class="fa fa-chevron-left"></i> 
        </a>
    
    </div>

    <div class="post-nav-prev post-nav-item">
    
        <a href="http://www.shutdown.cn/post/redis%E5%AE%95%E6%9C%BA%E6%8E%92%E6%9F%A5%E5%A4%8D%E7%9B%98/" rel="prev" title="">
         <i class="fa fa-chevron-right"></i>
        </a>
    
    </div>
</div>
      
     
     
     






    </footer>
  </article>
</section>

          </div>
        </div>
        <div class="sidebar-toggle">
  <div class="sidebar-toggle-line-wrap">
    <span class="sidebar-toggle-line sidebar-toggle-line-first"></span>
    <span class="sidebar-toggle-line sidebar-toggle-line-middle"></span>
    <span class="sidebar-toggle-line sidebar-toggle-line-last"></span>
  </div>
</div>
<aside id="sidebar" class="sidebar">
  <div class="sidebar-inner">

    <section class="site-overview sidebar-panel  sidebar-panel-active ">
      <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
    <img class="site-author-image" itemprop="image"
        src="http://www.shutdown.cn/img/author.jpg"
        alt="不与天斗Domino" />
    <p class="site-author-name" itemprop="name">不与天斗Domino</p>
    <p class="site-description motion-element" itemprop="description"> 
        Programmer &amp; Architect</p>
</div>
      <nav class="site-state motion-element">
    <div class="site-state-item site-state-posts">
      <a href="http://www.shutdown.cn/post/">
        <span class="site-state-item-count">183</span>
        <span class="site-state-item-name">日志</span>
      </a>
    </div>
    <div class="site-state-item site-state-categories">    
        <a href="http://www.shutdown.cn/categories/">      
         
        <span class="site-state-item-count">15</span>
        
        <span class="site-state-item-name">分类</span>
        
        </a>
    </div>

    <div class="site-state-item site-state-tags">
        <a href="http://www.shutdown.cn/tags/">
         
        <span class="site-state-item-count">224</span>
        
        <span class="site-state-item-name">标签</span>
        </a>
    </div>
</nav>
      
      

      

      <div class="links-of-blogroll motion-element inline">
<script type="text/javascript" src="//rf.revolvermaps.com/0/0/8.js?i=&amp;m=0&amp;s=220&amp;c=ff0000&amp;cr1=ffffff&amp;f=arial&amp;l=33&amp;bv=35" async="async"></script>
</div>

    </section>
    
  </div>
</aside>

      </div>
    </main>
   
    <footer id="footer" class="footer">
      <div class="footer-inner">
        <div class="copyright" >
  <span itemprop="copyrightYear">  &copy; 
  2013 - 2023</span>
  <span class="with-love"><i class="fa fa-heart"></i></span>
  <span class="author" itemprop="copyrightHolder">天地维杰网</span>
  <span class="icp" itemprop="copyrightHolder"><a href="https://beian.miit.gov.cn/" target="_blank">京ICP备13019191号-1</a></span>
</div>
<div class="powered-by">
  Powered by - <a class="theme-link" href="http://gohugo.io" target="_blank" title="hugo" >Hugo v0.63.2</a>
</div>
<div class="theme-info">
  Theme by - <a class="theme-link" href="https://github.com/xtfly/hugo-theme-next" target="_blank"> NexT
  </a>
</div>


      </div>
    </footer>

    <div class="back-to-top">
      <i class="fa fa-arrow-up"></i>
      <span id="scrollpercent"><span>0</span>%</span>
    </div>
  </div>

  

<script type="text/javascript">
  if (Object.prototype.toString.call(window.Promise) !== '[object Function]') {
    window.Promise = null;
  }
</script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/jquery/index.js?v=2.1.3"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/fastclick/lib/fastclick.min.js?v=1.0.6"></script> 
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/jquery_lazyload/jquery.lazyload.js?v=1.9.7"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/velocity/velocity.min.js?v=1.2.1"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/vendor/velocity/velocity.ui.min.js?v=1.2.1"></script>
<script src="http://www.shutdown.cn/js/vendor/ua-parser-js/dist/ua-parser.min.js?v=0.7.9"></script>

<script src="http://www.shutdown.cn/js/vendor/fancybox/jquery.fancybox.pack.js?v=2.1.5"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/utils.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/motion.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/affix.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/schemes/pisces.js"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/scrollspy.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/post-details.js"></script>
<script type="text/javascript" src="http://www.shutdown.cn/js/toc.js"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/bootstrap.js"></script>

<script type="text/javascript" src="http://www.shutdown.cn/js/search.js"></script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    extensions: ["tex2jax.js"],
    jax: ["input/TeX", "output/HTML-CSS"],
    tex2jax: {
      inlineMath: [ ['$','$'] ],
      displayMath: [ ['$$','$$'] ],
      processEscapes: true
    },
    "HTML-CSS": { fonts: ["TeX"] }
  });
</script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML' async></script>
</body>
</html>